/*******************************************************************************
**
** Filename:  DataModelValidator.java
**
** File Description:  This class acts as a Data Model element verifier.  It
**                    is used to verify that the set value of an LMSSetValue()
**                    request maps to the correct type of the element.  It
**                    is also used to verify that a return value from an LMS
**                    for an LMSGetValue() request maps to the correct type
**                    expected.
**
** Author: ADLI Project
**
** Company Name: Concurrent Technologies Corporation
**
** Module/Package Name:  org.adl.datamodel.cmi
** Module/Package Description: Collection of CMI Data Model objects
**
** Design Issues:
**
** Implementation Issues:
** Known Problems:
** Side Effects:
**
** References: AICC CMI Data Model
**             ADL SCORM
**
*******************************************************************************
**
** Concurrent Technologies Corporation (CTC) grants you ("Licensee") a non-
** exclusive, royalty free, license to use, modify and redistribute this
** software in source and binary code form, provided that i) this copyright
** notice and license appear on all copies of the software; and ii) Licensee
** does not utilize the software in a manner which is disparaging to CTC.
**
** This software is provided "AS IS," without a warranty of any kind.  ALL
** EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
** IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-
** INFRINGEMENT, ARE HEREBY EXCLUDED.  CTC AND ITS LICENSORS SHALL NOT BE LIABLE
** FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
** DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES.  IN NO EVENT WILL CTC  OR ITS
** LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT,
** INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
** CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF
** OR INABILITY TO USE SOFTWARE, EVEN IF CTC HAS BEEN ADVISED OF THE POSSIBILITY
** OF SUCH DAMAGES.
**
*******************************************************************************
**
** Date Changed   Author            Reason for Changes
** ------------   ----------------  -------------------------------------------
** 09/05/2000     ADLI Project      PT110 : The method checkString256() was
**                                          changed to checkString255() to
**                                          reflect the CMI 3.0.3 changes
**
** 09/07/2000     ADLI Project      PT112 : The method checkLocale() was removed
**                                          due to data model changes. CMILocale
**                                          elements now fall under the
**                                          CMIString255 category.
**
** 11/07/2000     ADLI Project      PT 297: Vocabulary lists and data types were
**                                   updated to match the SCORM 1.1
**                                   specifications.
**
** 11/15/2000     S. Thropp         PT 263: Removal of reference to SCORM
**                                  version.
**                                  PT 297: Cleaned up definition of Time and
**                                  Timespan.  The seconds for these two data
**                                  types must be in the following format:
**                                  SS.SS - five chars
**
** 12/15/2000     J. Poltrack       PT 297: The credit vocab member "no credit"
**                                  was changed to "no-credit"
**
** 12/27/2001     Jeff Falls        Added checkScorDecimal() to test the
**                                  "cmi.core.score" elements for negative
**                                  values
**
** 01/21/2001     Jeff Falls        Added check to checkScoreDecimal() to test
**                                  for normalized scores (0 <= n >= 100)
**
** 05/09/2002     S. Thropp         PT1828: Removed the convertString() function
**                                  This function is not used and it is private.
**                                  Also removed fixWord function, this again
**                                  is a private function that was only used
**                                  by the convertString function.
**
** 05/09/2002     S. Thropp         PT1832:  The checkIdentifier method returns
**                                  true for blank identifiers.  This is not
**                                  correct according to the SCORM.  Added
**                                  test to see if the identifier is blank
**
** 05/09/2002     S. Thropp         PT1833:  Changed checkTime to correctly
**                                  check the seconds part.  Code added to make
**                                  sure seconds has at least 2 decimals
**                                  (03:00:03 vs. 03:00:3)
**
** 05/14/2002     B. Capone         PT1833: Modified the hours/minutes if
**                                  statement in the try block of checkTime().
**                                  Only want to check seconds if hours and
**                                  minutes are within allowable range.
**
********************************************************************************/
package org.adl.datamodels;

//native java imports
import java.io.*;
import java.util.*;
import java.lang.reflect.*;

//adl imports
import org.adl.util.debug.*;
import org.adl.datamodels.cmi.*;

public class DataModelValidator implements Serializable
{
   // Hash table to hold the different vocabulary types found in the SCORM
   private static transient Hashtable vocabulary;

   /****************************************************************************
    **
    **  Method:  DataModelvalidator
    **  Input :  none
    **  Output:  none
    **
    **  Description:  Default constructor - Sets up the hash table with the
    **                following keys:
    **                   Mode, Status, Exit, WhyLeft, Credit, Entry,
    **                   TimeLimitAction, Interaction, and Result.
    **
    **                Each key maps to a set of valid values used for the
    **                keys vocabulary type
    **
    **                Mode - normal,review,browse
    **                Status - passed, completed, failed, incomplete, browsed
    **                         not attempted.
    ***************************************************************************/
   public DataModelValidator()
   {
      vocabulary = new Hashtable();

      vocabulary.put( "Mode",buildModeList() );
      vocabulary.put( "Status",buildStatusList() );
      vocabulary.put( "Exit",buildExitList() );
      vocabulary.put( "Credit",buildCreditList() );
      vocabulary.put( "Entry",buildEntryList() );
      vocabulary.put( "TimeLimitAction",buildTimeLimitActionList() );
      vocabulary.put( "Interaction",buildInteractionList() );
      vocabulary.put( "Result",buildResultList() );
   }

   /****************************************************************************
    **
    **  Method:  buildModeList()
    **  Input :  none
    **  Output:  String[] - list of members
    **
    **  Description: This method builds the list of valid Mode vocabulary
    **               members:
    **                   normal,review,browse
    ***************************************************************************/
   private String[] buildModeList()
   {
      int numItems = 3;
      String[] list = new String[numItems];

      list[0] = "normal";
      list[1] = "review";
      list[2] = "browse";
      return list;
   }

   /****************************************************************************
    **
    **  Method:  buildStatusList()
    **  Input :  none
    **  Output:  String[] - list of members
    **
    **  Description: This method builds the list of valid Status vocabulary
    **               members:
    **                 passed,completed,failed,incomplete,browsed,not attempted
    ***************************************************************************/
   private String[] buildStatusList()
   {
      int numItems = 6;
      String[] list = new String[numItems];

      list[0] = "passed";
      list[1] = "completed";
      list[2] = "failed";
      list[3] = "incomplete";
      list[4] = "browsed";
      list[5] = "not attempted";
      return list;
   }

   /****************************************************************************
    **
    **  Method:  buildExitList()
    **  Input :  none
    **  Output:  String[] - list of members
    **
    **  Description:  This method builds the list of valid Exit vocabulary
    **                members:
    **                  time-out, suspend, logout
    ***************************************************************************/
   private String[] buildExitList()
   {
      int numItems = 4;
      String[] list = new String[numItems];

      list[0] = "";
      list[1] = "time-out";
      list[2] = "suspend";
      list[3] = "logout";
      return list;
   }

   /****************************************************************************
    **
    **  Method:  buildCreditList()
    **  Input :  none
    **  Output:  String[] - list of members
    **
    **  Description: This method builds the list of valid Credit vocabulary
    **               members:
    **                   credit, no credit
    ***************************************************************************/
   private String[] buildCreditList()
   {
      int numItems = 2;
      String[] list = new String[numItems];

      list[0] = "credit";
      list[1] = "no-credit";
      return list;
   }

   /****************************************************************************
    **
    **  Method:  buildEntryList()
    **  Input :  none
    **  Output:  String[] - list of members
    **
    **  Description: This method builds the list of valid Entry vocabulary
    **               members:
    **                   ab-initio, resume
    ***************************************************************************/
   private String[] buildEntryList()
   {
      int numItems = 3;
      String[] list = new String[numItems];

      list[0] = "";
      list[1] = "ab-initio";
      list[2] = "resume";
      return list;
   }

   /****************************************************************************
    **
    **  Method:  buildTimeLimitActionList()
    **  Input :  none
    **  Output:  String[] - list of members
    **
    **  Description: This method builds the list of valid Time Limit Action
    **               vocabulary members:
    **                 "exit,message", "continue,message",
    **                 "exit,no message","continue,no message"
    ***************************************************************************/
   private String[] buildTimeLimitActionList()
   {
      int numItems = 5;
      String[] list = new String[numItems];

      list[0] = "";
      list[1] = "exit,message";
      list[2] = "exit,no message";
      list[3] = "continue,message";
      list[4] = "continue,no message";
      return list;
   }


   /****************************************************************************
    **
    **  Method:  buildInteractionList()
    **  Input :  none
    **  Output:  String[] - list of members
    **
    **  Description: This method builds the list of valid Interaction
    **               vocabulary members:
    **                true-false, multiple choice, fill in the blank,
    **                matching, simple performance, likert, sequencing,
    **               numeric
    ***************************************************************************/
   private String[] buildInteractionList()
   {
      int numItems = 8;
      String[] list = new String[numItems];

      list[0] = "true-false";
      list[1] = "choice";
      list[2] = "fill-in";
      list[3] = "matching";
      list[4] = "performance";
      list[5] = "likert";
      list[6] = "sequencing";
      list[7] = "numeric";
      return list;
   }

   /****************************************************************************
    **
    **  Method:  buildResultList()
    **  Input :  none
    **  Output:  String[] - list of members
    **
    **  Description: This method builds the list of valid Result
    **               vocabulary members:
    **                 correcct, wrong, unanticipated, neutral, x.x
    ***************************************************************************/
   private String[] buildResultList()
   {
      int numItems = 4;
      String[] list = new String[numItems];

      list[0] = "correct";
      list[1] = "wrong";
      list[2] = "unanticipated";
      list[3] = "neutral";

      return list;
   }

   /****************************************************************************
     **
     ** Method:  checkBlank()
     ** Input:   Element elementToBeChecked - Element to be checked
     **          String value - value to be set
     ** Output:  boolean result - indicates whether or not the value is
     **                           correct type according to element
     **
     ** Description: This method check to see if the value is blank (length
     **               of string is 0
     ***************************************************************************/
   public boolean checkBlank(Element elementToBeChecked,
                             String value)
   {
      boolean result = false;
      if ( value.length() == 0 )
      {
         result = true;
      }
      return result;
   }

   /****************************************************************************
    **
    ** Method:  checkBoolean()
    ** Input:   Element elementToBeChecked - Element to be checked
    **          String value - value to be checked
    ** Output:  boolean result - indicates whether or not the value is
    **                           correct type according to element
    **
    ** Description: This method checks to see if the value passed in
    **              is equal to either true or false.
    ***************************************************************************/
   public boolean checkBoolean(Element elementToBeChecked,
                               String value)
   {
      boolean flag = false;
      if ( (value.equals("true")) ||
           (value.equals("false")) )
      {
         flag = true;
      }
      return flag;
   }

   /****************************************************************************
    **
    ** Method:  checkFeedback()
    ** Input:   Element elementToBeChecked - Element to be checked
    **          String value - value to be checked
    ** Output:  boolean result - indicates whether or not the value is
    **                           correct type according to element
    **
    ** Description:
    ***************************************************************************/
   public boolean checkFeedback(Element elementToBeChecked,
                                String value)
   {
      return true;
   }

   /****************************************************************************
    **
    ** Method:  checkString255()
    ** Input:   Element elementToBeChecked - Element to be checked
    **          String value - value to be checked
    ** Output:  boolean result - indicates whether or not the value is
    **                           correct type according to element
    **
    ** Description: This method makes sure the value passed in is less
    **              than or equal to 255
    ***************************************************************************/
   public boolean checkString255(Element elementToBeChecked,
                                 String value)
   {
      boolean flag = false;
      if ( value.length() <= 255 )
      {
         flag = true;
      }

      return flag;
   }

   /****************************************************************************
    **
    ** Method:  checkString4096()
    ** Input:   Element elementToBeChecked - Element to be checked
    **          String setValue - value to be checked
    ** Output:  boolean result - indicates whether or not the value is
    **                           correct type according to element
    **
    ** Description: This method checks the value passed in to see if it
    **              is less than or equal to 4096
    ***************************************************************************/
   public boolean checkString4096(Element elementToBeChecked,
                                  String value)
   {
      boolean flag = false;
      if ( value.length() <= 4096 )
      {
         flag = true;
      }

      return flag;
   }

    /****************************************************************************
    **
    ** Method:  checkScoreDecimal()
    ** Input:   Element elementToBeChecked - the element to be checked
    **          String value - value to be checked
    ** Output:  boolean result - indicates whether or not the request is
    **                           valid
    **
    ** Description:  This method checks to make sure the value passed in
    **               is a valid decimal
    ***************************************************************************/
   public boolean checkScoreDecimal(Element elementToBeChecked,
                                    String value)
   {
      boolean result = false;

      Double zero = new Double(0.0);
      Double oneHundred = new Double(100.0);

      if ( !(checkBlank(elementToBeChecked, value)) )
      {
         try
         {
            Double tmpDouble = new Double(value);

            //Check for Normalized Score (0 <= n >= 100)
            if ( (tmpDouble.compareTo(zero) >= 0) &&
                 (tmpDouble.compareTo(oneHundred) <= 0) )
            {
               result = true;
            }
            else
            {
               if ( DebugIndicator.ON )
               {
                  System.out.println(value + " is not a normalized score");
                  System.out.println("0 <= " + value + " >= 100");
               }
            }
         }
         catch ( NumberFormatException nfe )
         {
            if ( DebugIndicator.ON )
            {
               System.out.println(value + " is not a valid CMIDecimal Type");
            }
         }

      }
      else
      {
          result = true;
      }

      return result;
   }



   /****************************************************************************
    **
    ** Method:  checkDecimal()
    ** Input:   Element elementToBeChecked - the element to be checked
    **          String value - value to be checked
    ** Output:  boolean result - indicates whether or not the request is
    **                           valid
    **
    ** Description:  This method checks to make sure the value passed in
    **               is a valid decimal
    ***************************************************************************/
   public boolean checkDecimal(Element elementToBeChecked,
                               String value)
   {
      boolean result = false;


      if ( !(checkBlank(elementToBeChecked, value)) )
      {

          try
          {
             Double tmpDouble = new Double(value);
             result = true;
          }
          catch ( NumberFormatException nfe )
          {
             if ( DebugIndicator.ON )
             {
                System.out.println(value + " is not a valid CMIDecimal Type ");
             }
          }
      }
      else
      {
          result = true;
      }


      return result;
   }


   /****************************************************************************
    **
    ** Method:  checkVocabulary
    ** Input:   Element elementToBeChecked - The element to be checked
    **          String value - value to be checked
    ** Output:  boolean result - indicates whether or not the request is
    **                           valid
    **
    ** Description:  This method checks to make sure the set value passed in
    **               is the correct vocabulary member of the element.
    ***************************************************************************/
   public boolean checkVocabulary(Element elementToBeChecked,
                                  String value)
   {

      boolean flag = false;
      String vocabType = elementToBeChecked.getVocabularyType();

      // find out the field being verified from the element
      String[] theList = (String[]) vocabulary.get(vocabType);

      for ( int i = 0; i < theList.length; i++ )
      {
         String tmpString = theList[i];

         if ( (tmpString.equals(value) == true) )
         {
            flag = true;
            break;
         }
      }


      //Check to see if the "Results" category is passing in a decimal number
      if ( (vocabType.equals("Result")) && (flag != true) )
      {
          flag = checkDecimal(elementToBeChecked, value);
      }


      // No match found
      if ( flag == false )
      {
         if ( DebugIndicator.ON )
         {
            System.out.println("[" + value + "] is not a valid vocabulary member ");
            System.out.println("for the " + vocabType + " Vocabulary Type");
         }
      }

      return flag;

   }

   /****************************************************************************
    **
    ** Method:  checkIdentifier
    ** Input:   Element elementToBeChecked - element to be checked
    **          String value - value to be checked
    **
    ** Output:  boolean result - indicates whether or not the request is
    **                           valid
    **
    ** Description: This method check to make sure the value passed in
    **              is a valid identifier.
    ***************************************************************************/
   public boolean checkIdentifier(Element elementToBeChecked,
                                  String value)
   {
      boolean flag = false;

      if ( (value.length() <= 255) &&
           (value.indexOf(' ') == -1) &&
           (!(checkBlank(elementToBeChecked,value))) )
      {
         flag = true;
      }

      return flag;
   }

   /****************************************************************************
    **
    ** Method:  checkInteger
    ** Input:   Element elementToBeChecked - element to be checked
    **          String value - value to be checked
    **
    ** Output:  boolean result - indicates whether or not the request is
    **                           valid
    **
    ** Description:  This method checks to make sure the setValue passed in
    **               is a valid integer
    ***************************************************************************/
   public boolean checkInteger(Element elementToBeChecked,
                               String value)
   {
      boolean flag = false;

      try
      {
         Integer tmpInt = new Integer(value);
         int theValue = tmpInt.intValue();

         if ( (theValue >= 0) &&
              (theValue <= 65536) )
         {
            flag = true;
         }
      }
      catch ( NumberFormatException nfe )
      {
         if ( DebugIndicator.ON )
         {
             System.out.println(value + "  has invalid format for an Integer");
         }
      }

      return flag;
   }

   /****************************************************************************
    **
    ** Method:  checkSInteger()
    ** Input:   Element elementToBeChecked - element to be checked
    **          String value - value to be checked
    **
    ** Output:  boolean result - indicates whether or not the request is
    **                           valid
    **
    ** Description:  This method checks to make sure that the value passed
    **               in is a valid Signed Integer
    ***************************************************************************/
   public boolean checkSInteger(Element elementToBeChecked,
                                String value)
   {
      boolean flag = false;
      try
      {
         Integer tmpInt = new Integer(value);
         int theValue = tmpInt.intValue();

         if ( (theValue >= -32768) &&
              (theValue <= 32768) )
         {
            flag = true;
         }
      }
      catch ( NumberFormatException nfe )
      {
         if ( DebugIndicator.ON )
         {
             System.out.println(value + " has invalid format for a Signed Integer");
         }
      }


      return flag;
   }

   /****************************************************************************
   **
   ** Method:  checkTime()
   ** Input:   Element elementToBeChecked - element to be checked
   **          String value - value to be checked
   **
   ** Output:  boolean result - indicates whether or not the request is
   **                           valid
   **
   ** Description: This method checks to make sure the value passed in
   **              is a valid Time   HH:MM:SS.S
   ***************************************************************************/
   public boolean checkTime(Element elementToBeChecked,
                            String value)
   {
      boolean flag = false;
      StringTokenizer stk = new StringTokenizer(value,":",false);

      //Should have 3 tokens: HH, MM, and SS.SS
      if ( stk.countTokens() == 3 )
      {
         String hour = stk.nextToken();
         String minute = stk.nextToken();
         String sec = stk.nextToken();

         if ( ( hour.length() == 2 ) &&
              ( minute.length() == 2) &&
              ( sec.length() <= 5) )
         {
            // Try to convert hours and minutes to integers and seconds to float.
            try
            {
               Integer hourAsInt = new Integer(hour);
               Integer minAsInt = new Integer(minute);

               if ( ( (hourAsInt.intValue() >= 0) &&
                      (hourAsInt.intValue() <= 24) ) &&
                    ( (minAsInt.intValue() >= 0) &&
                      (minAsInt.intValue() <= 60) ) )
               {
                  flag = true;

                  // Try to convert seconds.  If not, throw NumberFormatException.
                  // Validate Seconds, SS.SS
                  // Try to convert seconds.  If not, throw NumberFormatException.
                  StringTokenizer secTokenizer = new StringTokenizer(sec,".",false);
                  int numTok = secTokenizer.countTokens();

                  String secondsPart = secTokenizer.nextToken();

                  if ( DebugIndicator.ON )
                  {
                     System.out.println("secondsPart: " + secondsPart);
                  }

                  if ( secondsPart.length() == 2 )
                  {
                     Double secAsDouble = new Double(secondsPart);
                     flag = true;
                     if ( DebugIndicator.ON )
                     {
                        System.out.println("seconds part had 2 chars and a valid decimal");
                     }
                  }
                  else
                  {
                     flag = false;
                     if ( DebugIndicator.ON )
                     {
                        System.out.println("Seconds Part did not have 2 chars");
                     }
                  }
               }
            }
            catch ( NumberFormatException nfe )
            {
               if ( DebugIndicator.ON )
               {
                  System.out.println("Value could not be converted to a time");
               }

               flag = false;
            }
         } // end if correct size
      } // end if correct tokens

      if ( !flag )
      {
         if ( DebugIndicator.ON )
         {
            System.out.println("Value being used for setting: " + value +
                            " is not in Valid Time Format (HH:MM:SS.SS)" );
         }
      }

      return flag;
   }

   /****************************************************************************
    **
    ** Method:  checkTimespan()
    ** Input:   Element elementToBeChecked - element to be checked
    **          String value - value to be checked
    **
    ** Output:  boolean result - indicates whether or not the request is
    **                           valid
    **
    ** Description: This method checks to make sure the value passed in is
    **              a valid Timespan    HHHH:MM:SS.S
    ***************************************************************************/
   public boolean checkTimespan(Element elementToBeChecked,
                                String value)
   {
      boolean flag = false;

      if ( !(checkBlank(elementToBeChecked, value)) )
      {
         StringTokenizer stk = new StringTokenizer(value,":",false);

         if ( stk.countTokens() == 3 )
         {
            String hour = stk.nextToken();
            String minute = stk.nextToken();
            String sec = stk.nextToken();

            if ( ( (hour.length() >= 2) && (hour.length() <= 4) ) &&
                   (minute.length() == 2) &&
                   (sec.length() <= 5) )
            {
               // Check to see if the inputted value can be changed to a "Timespan"
               try
               {
                  // Try to convert hours and minutes. If not, throw
                  // NumberFormatException.
                  Integer hourAsInt = new Integer(hour);
                  Integer minAsInt = new Integer(minute);


                  // Make sure hours and minutes are in range
                  if ( ( (hourAsInt.intValue() >= 0) &&
                         (hourAsInt.intValue() <= 9999) ) &&
                       ( (minAsInt.intValue() >= 0) &&
                         (minAsInt.intValue() <= 99) ) )
                  {
                     flag = true;
                  }

                  // Validate Seconds, SS.SS
                  // Try to convert seconds.  If not, throw NumberFormatException.
                  StringTokenizer secTokenizer = new StringTokenizer(sec,".",false);
                  int numTok = secTokenizer.countTokens();

                  String secondsPart = secTokenizer.nextToken();

                  if ( DebugIndicator.ON )
                  {
                      System.out.println("secondsPart: " + secondsPart);
                  }

                  if ( secondsPart.length() == 2 )
                  {
                     Double secAsDouble = new Double(secondsPart);
                     flag = true;
                     if ( DebugIndicator.ON )
                     {
                         System.out.println("seconds part had 2 chars and a valid decimal");
                     }
                  }
                  else
                  {
                     flag = false;
                     if ( DebugIndicator.ON )
                     {
                        System.out.println("Seconds Part did not have 2 chars");
                     }
                  }

                  // If numTok == 2 then there is a decimal part
                  if ( numTok == 2  && flag)
                  {
                     String decimalPart = secTokenizer.nextToken();
                     if ( decimalPart.length() <= 2)
                     {
                        Double decAsDouble = new Double(decimalPart);
                        flag = true;
                     }
                     else
                     {
                        flag = true;
                     }
                  }

               }
               catch ( NumberFormatException nfe )
               {
                  if ( DebugIndicator.ON )
                  {
                     System.out.println("Value could not be converted to a time");
                  }

                  // Invalid Date Format
                  flag = false;
               }
            } // ends the check for appropriate size
         } // Ends if value has 3 tokens
      }
      else
      {
         flag = true;
      }

      if ( !flag )
      {
         if ( DebugIndicator.ON )
         {
            System.out.println("Value being used for setting: " + value +
                               " is not in Valid Time Format (HHHH:MM:SS.SS)" );
         }
      }

      return flag;
   }

} // end of DataModelValidator

